Skip to content

feat(tui): localize /mode picker and composer Vim indicator (harvest of #2239)#3530

Merged
Hmbown merged 1 commit into
mainfrom
feat/i18n-mode-picker
Jun 24, 2026
Merged

feat(tui): localize /mode picker and composer Vim indicator (harvest of #2239)#3530
Hmbown merged 1 commit into
mainfrom
feat/i18n-mode-picker

Conversation

@Hmbown

@Hmbown Hmbown commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Credited harvest of the remaining user-facing mode-system slice from @gordonlu's #2239 ("i18n Phase 1–4b wiring"), remade against current main rather than merged.

Why a remake instead of a merge

#2239's base is 1658 commits behind main (26 conflicting files). Crucially, most of it already landed on main via @gordonlu's later PRs (#2891 approval, #2892 elevation, #2896 status picker, #2901 tool families) and earlier harvests (FanoutCounts, queue, context inspector). Main now ships 7 complete locales (en, ja, zh-Hans, zh-Hant, pt-BR, es-419, vi) enforced by a completeness test. The genuine un-harvested remainder was the mode system — exactly gordon's branch focus (i18n-phase1-composer-config-statusline). So this rebuilds that surface natively on current main.

What this does

  • Localizes the /mode picker: prompt, mode names (Agent/YOLO/Plan), and hints — the screen where users learn what each mode does.
  • Localizes the composer Vim indicator (-- NORMAL/INSERT/VISUAL --).
  • Adds 10 MessageIds with translations across all 7 shipped locales. Translations convey meaning (e.g. YOLO hint → "full access" / auto-approve; Agent hint → asks before changes), staying close to the English where natural. English copy is unchanged.

Safety: model boundary untouched

AppMode::label() (footer + telemetry + context report — model-facing) keeps returning the English &'static str. The picker/composer use additive *_localized(locale) helpers driven by ui_locale, so the localized strings can't leak into model-facing context — the exact bug flagged on the original PR. The superseded English-only picker_hint() / VimMode::label() are removed (their values now live in the En locale arm).

Tests

  • shipped_first_pack_has_no_missing_core_messages — all 7 locales complete (exhaustive matches make a missing arm a compile error).
  • New mode_picker_strings_are_translated_in_non_english_locales — no English leak for the picker prompt + hints.
  • cargo fmt --all --check, git diff --check, zero warnings; mode_picker (4), tui::app::tests (169), commands::groups::core (117) all green.

Credit

Authored as @gordonlu (canonical noreply) + Harvested from PR #2239 by @gordonlu; CONTRIBUTORS.md and CHANGELOG.md updated. Closes the mode-system portion of #2239 — the remaining surfaces (e.g. error/error_msg refactor) are intentionally out of scope.


Generated by Claude Code

@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Code Review in Progress

  • Gather context and understand request
  • Read changed files and diff
  • Check localization correctness and completeness
  • Verify model-boundary safety claim
  • Check for reuse/simplification opportunities
  • Review test coverage
  • Post final review

View job run

@Hmbown Hmbown force-pushed the feat/i18n-mode-picker branch from e9350aa to 86b136c Compare June 24, 2026 02:39
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @Hmbown's task in 3m 22s —— View job


Code Review — feat(tui): localize /mode picker and composer Vim indicator

  • Gather context and understand request
  • Read changed files and diff
  • Check localization correctness and completeness
  • Verify model-boundary safety claim
  • Check for reuse/simplification opportunities
  • Review test coverage
  • Post final review

Summary

The harvest is clean and well-scoped. Model-boundary safety holds. One visible cosmetic regression for CJK users in the picker layout; everything else is correct.


Model-boundary safety ✓

AppMode::label() (used by context_report.rs:370, relay.rs:59, session.rs, engine.rs, debug.rs, etc.) is untouched and continues to return English &'static str. The new display_name_localized() / picker_hint_localized() / label_localized() are purely additive helpers driven by ui_locale and are never called from model-facing paths. The claim holds.


Issues

1. CJK picker alignment regression — mode_picker.rs:154 (cosmetic but user-visible)

format!(
    "{pointer} {}. {:<7}",
    mode.number(),
    mode.display_name_localized(self.locale)
),

{:<7} pads to 7 Unicode scalar values, not 7 terminal columns. For CJK mode names where every character occupies 2 columns:

  • "エージェント" — 6 scalars → padded to 7 scalars, but 13 terminal columns
  • "智能体" — 3 scalars → padded to 7 scalars, but 10 terminal columns

This pushes the hint span rightward by 5–6 extra columns in East Asian locales, breaking the visual alignment for the only three users who care most about localization. unicode_width is already a dependency in the crate (imported in widgets/mod.rs).

Fix — replace the padded format with explicit width-aware padding:

let name = mode.display_name_localized(self.locale);
let display_cols = UnicodeWidthStr::width(name);
let pad = " ".repeat(7usize.saturating_sub(display_cols));
format!("{pointer} {}. {name}{pad}", mode.number()),

Fix this →


2. display_name() still present and unlocalized (out of scope, follow-up)

config.rs:1613–1615 uses mode.display_name() in the command response text:

"Switched to Agent mode."   // always English, even for ja/zh-Hans users
"Already in Plan mode."

Not a regression (was English before), but worth a follow-up i18n pass for the /mode command response. No action needed in this PR.


Localization quality

All 7 locales have translations for all 10 new MessageIds. The translations accurately convey semantics:

  • YOLO hints correctly describe auto-approve + shell access across all locales
  • CJK translations helpfully add "(フルアクセス)" / "(完全访问)" / "(完全存取)" to reinforce the risk — minor divergence from the English but beneficial
  • The Vietnamese "-- TRỰC QUAN --" for -- VISUAL -- is semantically loose (literally "intuitive/direct", not "visual selection"), but acceptable given Vim terminology doesn't have a canonical Vietnamese translation

Tests ✓

  • shipped_first_pack_has_no_missing_core_messages — passes because all 10 IDs are added to ALL_MESSAGE_IDS and all 7 locale functions have the new arms
  • mode_picker_strings_are_translated_in_non_english_locales — correctly tests only sentence-type messages (prompt + hints); mode names are excluded because "YOLO" and "Plan" legitimately stay identical across some locales
  • Existing mode picker tests updated with Locale::En arg — no behavior change

Dead code / cleanup ✓

No remaining callers of VimMode::label() or AppMode::picker_hint() — both are cleanly gone. The display_name() unlocalized method remains but is used appropriately for non-picker contexts.


Verdict: One fix needed for CJK display-width alignment before this is pixel-perfect. Everything else is correct and the model-boundary claim is solid.

@Hmbown Hmbown force-pushed the feat/i18n-mode-picker branch from 86b136c to 42491c3 Compare June 24, 2026 02:45
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @Hmbown's task in 1m 34s —— View job


Re-review Complete

  • Gather context and understand request
  • Check if CJK alignment fix from prior review was applied
  • Re-read changed files for any new issues
  • Post updated review

Updated Review — feat(tui): localize /mode picker and composer Vim indicator

This sync addresses the one issue flagged in the prior review. All checks pass.


CJK alignment fix ✓ — mode_picker.rs:152–158

The {:<7} scalar-count padding has been replaced with the UnicodeWidthStr::width-based approach suggested in the last review:

let name = mode.display_name_localized(self.locale);
// Pad by terminal columns, not scalar count, so wide (CJK) mode
// names keep the hint column aligned.
let pad = " ".repeat(7usize.saturating_sub(UnicodeWidthStr::width(name)));

This correctly computes display-column width for CJK mode names (e.g. "エージェント" = 12 cols, not 6 scalars). Alignment is now pixel-perfect across all 7 locales.


Composer Vim indicator also correct ✓ — widgets/mod.rs:2143–2169

composer_top_right_chrome() calls label_localized(app.ui_locale) at both call sites and measures the result with UnicodeWidthStr::width(vim_label) before placing it. CJK -- NORMAL/INSERT -- variants will not overflow the border area.


Model-boundary safety ✓

AppMode::label() (app.rs:935) remains an English &'static str unchanged. The new display_name_localized(), picker_hint_localized(), and label_localized() helpers are purely additive, driven by ui_locale, and unreachable from model-facing paths. The invariant holds.


Localization completeness ✓

All 10 MessageIds present and correctly placed in all 7 shipped locales (en, ja, zh-Hans, zh-Hant, pt-BR, es-419, vi). The exhaustive match arms in each locale function ensure any missing arm is a compile error.


Tests ✓

  • shipped_first_pack_has_no_missing_core_messages — covers all 10 new IDs
  • mode_picker_strings_are_translated_in_non_english_locales — verifies no English leak for sentence-type messages (prompt + hints)
  • Picker unit tests updated with Locale::En arg — no behavior regression

Verdict: Ready to merge. The CJK display-width fix is correct and complete; no remaining issues.

@Hmbown

Hmbown commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Heads-up for merge: please rebase or merge-commit, not squash. This commit carries Harvested from PR #2239 by @gordonlu (and gordonlu is correctly the author), but a squash rewrites the body and can drop that line — silently losing both the machine credit and the auto-close for #2239. Rebase keeps both intact (just demonstrated on #3529#3524 auto-closing @cyq1017 with credit). The rule is now written down in #3533.

Minor, separate: the commit's committer is Claude <noreply@anthropic.com> with a Claude-Session: trailer — bot traces the credit gate doesn't inspect (it only checks author + co-author trailers). Worth stripping before merge to match the no-bot-trailer rule in AGENTS.md; it doesn't affect gordonlu's credit.

Forward-ports the mode-system slice of the Phase 1 i18n work onto current
main. The /mode picker (prompt, mode names, and hints) and the composer's
Vim mode indicator now resolve through tr() across all seven shipped
locales. English copy is unchanged.

Model-facing surfaces stay English on purpose: AppMode::label() (footer,
telemetry, context report) keeps returning the English &'static str, so the
picker/composer use locale-aware *_localized helpers while the model
boundary is untouched. The superseded English-only picker_hint()/
VimMode::label() are removed now that their values live in the En locale arm.

Adds 10 MessageIds (mode picker prompt/names/hints + Vim Normal/Insert/
Visual) with translations in en, ja, zh-Hans, zh-Hant, pt-BR, es-419, vi,
satisfying the shipped-locale completeness test, plus a no-English-leak test
for the picker sentences.

Most of PR #2239 already landed on main via @gordonlu's later PRs (#2891,
#2892, #2896, #2901) and earlier harvests; this is the remaining
user-facing mode-system surface, remade against current main.

Harvested from PR #2239 by @gordonlu.
@Hmbown Hmbown force-pushed the feat/i18n-mode-picker branch from 42491c3 to 8f6a73d Compare June 24, 2026 02:53
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @Hmbown's task in 1m 35s —— View job


Bot Trailer — Already Clean

  • Gather context and understand request
  • Inspect commit for Claude-Session: trailer and bot committer
  • Confirm current state is compliant — no action needed

The synchronize event was triggered by an amended push that already resolved the issue Hunter flagged. The commit is now fully compliant:

Field Value
Author gordonlu <3125629+gordonlu@users.noreply.github.com>
Committer gordonlu <3125629+gordonlu@users.noreply.github.com>
Trailer Harvested from PR #2239 by @gordonlu
Bot trailers None — Claude-Session:, Co-authored-by: Claude both absent ✓

No Co-authored-by: or Claude-Session: trailers are present in the commit body. The check-coauthor-trailers.py gate will pass. Credit attribution and the Harvested from PR #2239 auto-close line are intact.

The branch is ready to merge via rebase or merge-commit (not squash), as Hunter noted.

Hmbown commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Both addressed:

I've got the merge — will land it via rebase as soon as CI is green, so please leave the merge button to me to avoid an accidental squash.


Generated by Claude Code

@Hmbown Hmbown merged commit 28bde4a into main Jun 24, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants